// Copyright 2012 Jeremy Wall

// Package rrd implements some Go bindings for librrd using the threadsafe
// variants of the functions.
package gorrd

/*
#cgo LDFLAGS: -lrrd_th
#include <stdlib.h>
#include <rrd.h>

 rrd_blob_t get_u_blo(rrd_infoval_t info) {
  return info.u_blo;
 }

 int get_u_cnt(rrd_infoval_t info) {
  return info.u_cnt;
 }

 time_t int64_to_time_t(long t) {
  return (time_t)t;
 }
*/
import "C"

import (
	"fmt"
	"io"
	"reflect"
	"time"
	"unsafe"
)

// TODO(jwall): Higher level wrappers for the various archive and template
// specifications to enforce some of the invariants rrdtool expects.

// CreateRRD creates an rrd database
//
// filename is the location of the rrd database to create
// step is the stepsize in seconds
// start is the time the database starts at. Updates previous
// to this are ignored.
// datasources are a list of strings describing the set of datasources to record
// in this rrd database.
func Create(filename string, step int64, start time.Time, datastores ...string) error {
	f, st, t := C.CString(filename), C.ulong(step), C.int64_to_time_t(C.long(start.Unix()))
	i, ar := toCStringArray(datastores)
	defer func() {
		C.free(unsafe.Pointer(f))
		for _, v := range ar {
			C.free(unsafe.Pointer(v))
		}
	}()
	return intToError(C.rrd_create_r(f, st, t, i, castCStringArrayToPointer(ar)))
}

// UpdateRRD updates a particular rrd database with a template and
// a set of values.
//
// filename is the location of the rrd database.
// template is a string in the form "ds-name:ds-name:..." indicating
// the order the values are updating the various datastores in.
func Update(filename, template string, values ...string) error {
	f, tpl := C.CString(filename), C.CString(template)
	i, vs := toCStringArray(values)
	defer func() {
		C.free(unsafe.Pointer(f))
		C.free(unsafe.Pointer(tpl))
		for _, v := range vs {
			C.free(unsafe.Pointer(v))
		}
	}()
	return intToError(C.rrd_update_r(f, tpl, i, castCStringArrayToPointer(vs)))
}

// TSEntry is an entry in a timeseries from an rrd database.
type TSEntry struct {
	// Timestamp for the entry
	Ts time.Time
	// Value for the entry
	V int64
}

func (ts TSEntry) String() string {
	return fmt.Sprintf("{\"%d\": %d}", ts.Ts.Unix(), ts.V)
}

// Fetch fetches data from an rrd database.
//
// filename is the location of the rrd database you want to fetch from.
// agg is the consolidation function to use for the fetch.
// startT and endT are the start and end times for the fetch.
// step is the step size in seconds to use for the fetch.
func Fetch(filename, agg string, startT, endT time.Time, step int64) (map[string][]*TSEntry, error) {
	f, cf := C.CString(filename), C.CString(agg)
	startv, endv := startT.Unix(), endT.Unix()
	start, end := C.int64_to_time_t(C.long(startv)), C.int64_to_time_t(C.long(endv))
	var ds_cnt int64
	var ds_namv **C.char
	var data *int64
	defer func() {
		C.free(unsafe.Pointer(f))
		C.free(unsafe.Pointer(cf))
		C.free(unsafe.Pointer(data))
		C.free(unsafe.Pointer(ds_namv))
	}()
	C.rrd_fetch_r(f, cf, &start, &end,
		(*C.ulong)(unsafe.Pointer(&step)), (*C.ulong)(unsafe.Pointer(&ds_cnt)),
		(***C.char)(unsafe.Pointer(&ds_namv)), (**C.rrd_value_t)(unsafe.Pointer(&data)))
	dataLen := (endv - startv) / step
	var dataSlice []*int64
	var dsNames []**C.char
	attachCArray(
		(*reflect.SliceHeader)((unsafe.Pointer(&dataSlice))), int(dataLen/ds_cnt),
		uintptr(unsafe.Pointer(&data)))
	ret := make(map[string][]*TSEntry)
	attachCArray(
		(*reflect.SliceHeader)((unsafe.Pointer(&dsNames))), int(ds_cnt),
		uintptr(unsafe.Pointer(&ds_namv)))
	for _, dsi := range dsNames {
		name := C.GoString(*dsi)
		sl := make([]*TSEntry, dataLen)
		var ii int
		ts := startT
		for i := 0; i < int(dataLen); i++ {
			sl[ii] = &TSEntry{Ts: ts, V: *dataSlice[ii]}
			ii++
		}
		ret[name] = sl
	}
	return ret, intToError(C.rrd_test_error())
}

// RenderGraph renders an rrd graph with given arguments to the io.Writer.
func RenderGraph(w io.Writer, step int64, start, end time.Time, args ...string) (int, int, error) {
	var grinfo *C.rrd_info_t
	var xsize, ysize int
	argList := make([]string, 0, 5+len(args))
	argList = append(argList, "graph", "-", fmt.Sprintf("--start=%d", start.Unix()),
		fmt.Sprintf("--end=%d", end.Unix()), fmt.Sprintf("--step=%d", step))
	argList = append(argList, args...)
	argc, argv := toCStringArray(argList)
	defer func() {
		for _, v := range argv {
			C.free(unsafe.Pointer(v))
		}
	}()
	grinfo = C.rrd_graph_v(argc, castCStringArrayToPointer(argv))
	if grinfo == nil {
		return xsize, ysize, intToError(C.rrd_test_error())
	}
	walker := grinfo
	for walker != nil {
		key := C.GoString(walker.key)
		switch key {
		case "image_width":
			xsize = int(C.get_u_cnt(walker.value))
		case "image_height":
			ysize = int(C.get_u_cnt(walker.value))
		case "image":
			blob := C.get_u_blo(walker.value)
			bs := C.GoBytes(unsafe.Pointer(blob.ptr), C.int(blob.size))
			_, err := w.Write(bs)
			if err != nil {
				return xsize, ysize, err
			}
		}
		walker = (*C.rrd_info_t)(walker.next)
	}
	C.rrd_info_free(grinfo)
	return xsize, ysize, nil
}

// Graph prints out a graph to a file.
//
// filename the file to print the graph to.
// step is the size of the step in seconds to use for the graph
// start and end are the start and times for the graph
// args are the arguments you would to send the rrdtool graph or graphv
// commands. excluding the --start, --end, and --step ones.
// Returns the xsize, ysize of the image and an error if there was one.
func Graph(filename string, step int64, start, end time.Time, args ...string) (int, int, error) {
	var xsize, ysize int
	var ymin, ymax int64
	// create an argv set to pass
	argList := make([]string, 0, 5+len(args))
	argList = append(argList, "graph", filename, fmt.Sprintf("--start=%d", start.Unix()),
		fmt.Sprintf("--end=%d", end.Unix()), fmt.Sprintf("--step=%d", step))
	argList = append(argList, args...)
	argc, argv := toCStringArray(argList)
	var calcpr **C.char
	defer func() {
		for _, v := range argv {
			C.free(unsafe.Pointer(v))
		}
		C.free(unsafe.Pointer(calcpr))
	}()
	C.rrd_graph(argc, castCStringArrayToPointer(argv), (***C.char)(unsafe.Pointer(&calcpr)),
		(*C.int)(unsafe.Pointer((&xsize))),
		(*C.int)(unsafe.Pointer(&ysize)), nil, (*C.double)(unsafe.Pointer(&ymin)),
		(*C.double)(unsafe.Pointer(&ymax)))
	e := C.rrd_test_error()
	if int(e) != 0 {
		return xsize, ysize, fmt.Errorf("error: %q", C.GoString(C.rrd_get_error()))
	}
	return xsize, ysize, nil
}
